# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import math
import time
import traceback
import pandas as pd
from datetime import datetime
from utils.commons import retry_wrapper
from config import utc_offset, stable_symbol, glob_var


# =====获取数据
# 获取单个币种的1天数据
def fetch_binance_candle_data(exchange, symbol, run_time, limit=1500, interval='1h', symbol_type='swap'):
    # ===获取K线数据
    limit = 1000 if limit > 1000 and symbol_type == 'spot' else limit  # 现货最多获取1000根K
    # 计算获取k线的开始时间
    start_time_dt = run_time - pd.to_timedelta(interval) * limit
    params = {
        'symbol': symbol,      # 获取币种
        'interval': interval,  # 获取k线周期
        'limit': limit,        # 获取多少根
        'startTime': int(time.mktime(start_time_dt.timetuple())) * 1000  # 获取币种开始时间
    }
    # 获取指定币种的k线数据
    try:
        if symbol_type == 'swap':
            kline = retry_wrapper(exchange.fapiPublic_get_klines, params=params, func_name='获取币种K线', if_exit=False)
        else:
            kline = retry_wrapper(exchange.public_get_klines, params=params, func_name='获取币种K线', if_exit=False)
    except Exception as e:
        print(traceback.format_exc())
        # 如果获取k线重试出错，直接返回，当前币种不参与交易
        return pd.DataFrame()

    # ===整理数据
    # 将数据转换为DataFrame
    df = pd.DataFrame(kline, dtype='float')
    # 对字段进行重命名，字段对应数据可以查询文档（https://binance-docs.github.io/apidocs/futures/cn/#k）
    columns = {0: 'candle_begin_time', 1: 'open', 2: 'high', 3: 'low', 4: 'close', 5: 'volume', 6: 'close_time', 7: 'quote_volume',
               8: 'trade_num', 9: 'taker_buy_base_asset_volume', 10: 'taker_buy_quote_asset_volume', 11: 'ignore'}
    df.rename(columns=columns, inplace=True)

    df['symbol'] = symbol  # 添加symbol列
    df['symbol_type'] = symbol_type  # 添加类型字段
    df.sort_values(by=['candle_begin_time'], inplace=True)  # 排序
    df.drop_duplicates(subset=['candle_begin_time'], keep='last', inplace=True)  # 去重

    # 转换数据时区
    df['candle_begin_time'] = pd.to_datetime(df['candle_begin_time'], unit='ms')

    # 删除runtime那根未走完的k线数据（交易所有时候会返回这条数据）
    df = df[df['candle_begin_time'] + pd.Timedelta(hours=utc_offset) < run_time]
    df.reset_index(drop=True, inplace=True)

    return df


# 获取所有币种永续合约数据的1小时K线数据
def fetch_all_binance_candle_data(exchange, symbol_list, run_time, limit, interval='1h', symbol_type='swap'):
    """
    获取所有币种永续合约数据的1天K线数据

    :param exchange:    交易所对象，用于获取数据
    :param symbol_list: 币种泪飙
    :param run_time:    当前运行时间
    :param limit:       请求k线数量
    :param interval:    获取k线的周期
    :param symbol_type: 请求数据类型swap/spot
    :return:

    {
    'BTCUSDT':
                            symbol  ... taker_buy_quote_asset_volume
            0     BTCUSDT  ...                 1.451404e+08
            1     BTCUSDT  ...                 1.492456e+08
            2     BTCUSDT  ...                 1.200780e+08
            3     BTCUSDT  ...                 9.680288e+07
            4     BTCUSDT  ...                 6.867702e+08
            ...       ...  ...                          ...
            1495  BTCUSDT  ...                 1.858995e+08
            1496  BTCUSDT  ...                 1.151737e+08
            1497  BTCUSDT  ...                 8.091855e+07
            1498  BTCUSDT  ...                 1.037028e+08
            1499  BTCUSDT  ...                 1.111743e+07,

    'ETHUSDT':
            symbol  ... taker_buy_quote_asset_volume
            0     ETHUSDT  ...                 2.023519e+08
            1     ETHUSDT  ...                 1.813869e+08
            2     ETHUSDT  ...                 1.298206e+08
            3     ETHUSDT  ...                 1.544976e+08
            4     ETHUSDT  ...                 6.494550e+08
            ...       ...  ...                          ...
            1495  ETHUSDT  ...                 2.792866e+08
            1496  ETHUSDT  ...                 1.220917e+08
            1497  ETHUSDT  ...                 7.935349e+07
            1498  ETHUSDT  ...                 1.557781e+08
            1499  ETHUSDT  ...                 2.241793e+07,
    ......
    }
    """
    # 这里用dict存储，方便后面数据处理操作
    result = {}
    for symbol in symbol_list:
        # 获取k线数据
        df = fetch_binance_candle_data(exchange, symbol, run_time, limit, interval=interval, symbol_type=symbol_type)
        # 返回None或者空的df，不放到result里
        if df is None or df.empty:
            continue
        # 将数据添加到result中
        result[symbol] = df

    return result


# 获取币安的ticker数据
def fetch_binance_ticker_data(exchange, symbol_type='swap'):
    # 获取所有币种的ticker数据
    if symbol_type == 'swap':
        tickers = retry_wrapper(exchange.fapiPublic_get_ticker_price, func_name='获取所有合约币种的ticker数据')
    else:
        tickers = retry_wrapper(exchange.public_get_ticker_price, func_name='获取所有现货币种的ticker数据')
    tickers = pd.DataFrame(tickers, dtype=float)
    tickers.set_index('symbol', inplace=True)

    return tickers['price']


def load_market(exchange, symbol_type='swap'):
    """
    加载市场数据

    :param exchange:    交易所对象，用于获取数据
    :param symbol_type: 获取swap/spot市场数据，默认获取swap合约
    :return:

    min_qty         最小下单精度    例： 3 代表 0.001
        {'BTCUSDT': 3, 'ETHUSDT': 3, 'BCHUSDT': ,....}3
    price_precision 币种价格精     例： 2 代表 0.01
        {'BTCUSDT': 1, 'ETHUSDT': 2, 'BCHUSDT': 2, 'XRPUSDT': 4,...}
    min_notional    最小下单金额    例： 5.0 代表 最小下单金额是5U
        {'BTCUSDT': 5.0, 'ETHUSDT': 5.0, 'BCHUSDT': 5.0, 'XRPUSDT': 5.0...}
    """
    now_ms = datetime.now().timestamp() * 1000
    if symbol_type == 'swap':
        # ===获取交易对的信息
        exchange_info = retry_wrapper(exchange.fapiPublic_get_exchangeinfo, params={'timestamp': ''}, func_name='获取合约交易对的信息')

        # ===挑选出所有符合条件的交易对
        symbol_dict_list = list(filter(lambda s: ((now_ms - int(s['onboardDate'])) / 1000 / 86400) >= 7  # 挑选上架时间超过7天的交易对
                                             and s['status'] == 'TRADING'  # 挑选交易状态的交易对
                                             and s['quoteAsset'] == 'USDT'  # 挑选U本位结算的交易对
                                             and s['contractType'] == 'PERPETUAL', exchange_info['symbols']))
        symbol_list = [x['symbol'] for x in symbol_dict_list]  # 获取所有的交易对名称
    else:
        # ===获取交易对的信息
        exchange_info = retry_wrapper(exchange.public_get_exchangeinfo, func_name='获取现货交易对的信息')

        # ===挑选出所有符合条件的交易对
        symbol_dict_list = list(filter(lambda s: s['status'] == 'TRADING'  # 挑选交易状态的交易对
                                                 and s['quoteAsset'] == 'USDT'  # 挑选U本位结算的交易对，只能能USDT交易
                                       , exchange_info['symbols']))
        symbol_list = [x['symbol'] for x in symbol_dict_list]  # 获取所有的交易对名称

    # 更新与交易所的时间差
    diff_timestamp = int(datetime.now().timestamp() * 1000) - int(exchange_info['serverTime'])
    glob_var.update_diff_time(diff_timestamp)

    # 过滤稳定币和杠杆代币
    symbol_list = [x for x in symbol_list if not (x.replace('USDT', '').endswith(('UP', 'DOWN', 'BEAR', 'BULL')) or x.replace('USDT', '') in stable_symbol)]
    symbol_list = list(set(symbol_list))  # 去重。这里是严谨点处理，防止交易所出问题

    # ===获取各个交易对的精度、下单量等信息
    min_qty = {}  # 最小下单精度，例如bnb，一次最少买入0.001个
    price_precision = {}  # 币种价格精，例如bnb，价格是158.887，不能是158.8869
    min_notional = {}  # 最小下单金额，例如bnb，一次下单至少买入金额是5usdt
    # 遍历获得想要的数据
    for info in exchange_info['symbols']:
        symbol = info['symbol']
        for _filter in info['filters']:
            if _filter['filterType'] == 'PRICE_FILTER':
                price_precision[symbol] = int(math.log(float(_filter['tickSize']), 0.1))
            elif _filter['filterType'] == 'LOT_SIZE':
                min_qty[symbol] = int(math.log(float(_filter['minQty']), 0.1))
            elif _filter['filterType'] == 'MIN_NOTIONAL' and symbol_type == 'swap':  # 合约的最小下单金额
                min_notional[symbol] = float(_filter['notional'])
            elif _filter['filterType'] == 'NOTIONAL' and symbol_type == 'spot':  # 现货的最小下单金额
                min_notional[symbol] = float(_filter['minNotional'])

    return symbol_list, min_qty, price_precision, min_notional


# 重置一下页面最大杠杆
def reset_leverage(exchange, max_leverage=3, coin_list=[]):
    """
    重置一下页面最大杠杆
    :param exchange:        交易所对象，用于获取数据
    :param max_leverage:    设置页面最大杠杆
    :param coin_list:       对指定币种进行调整页面杠杆
    """
    # 获取账户持仓风险（这里有杠杆数据）
    position_risk = retry_wrapper(exchange.fapiPrivateV2_get_positionrisk, params={'timestamp': ''}, func_name='获取账户持仓风险')
    position_risk = pd.DataFrame(position_risk)  # 将数据转成DataFrame
    if len(coin_list) > 0:
        position_risk = position_risk[position_risk['symbol'].isin(coin_list)]  # 只对选币池中的币种进行调整页面杠杆
    position_risk.set_index('symbol', inplace=True)  # 将symbol设为index

    # 遍历每一个可以持仓的币种，修改页面最大杠杆
    for symbol, row in position_risk.iterrows():
        if int(row['leverage']) != max_leverage:
            # 设置杠杆
            retry_wrapper(exchange.fapiPrivate_post_leverage,
                          params={'symbol': symbol, 'leverage': max_leverage, 'timestamp': ''},
                          func_name='设置杠杆')

    print('修改页面最大杠杆操作完成')


# 检查是否开启了联合保证金模式
def check_multi_assets_margin(exchange):
    """
    检查是否开启了联合保证金模式

    :param exchange:        交易所对象，用于获取数据
    """
    # 查询保证金模式
    res = retry_wrapper(exchange.fapiPrivate_get_multiassetsmargin, params={'timestamp': ''}, func_name='fapiPrivate_get_multiassetsmargin', if_exit=False)
    # 判断是否开启了联合保证金模式
    if not bool(res['multiAssetsMargin']):  # 若联合保证金模式没有开启，则调用接口开启一下联合保证金模式
        params = {'multiAssetsMargin': 'true', 'timestamp': ''}
        retry_wrapper(exchange.fapiPrivate_post_multiassetsmargin, params=params, func_name='fapiPrivate_post_multiassetsmargin', if_exit=False)
        print('开启联合保证金模式')


# 检查是否是单向持仓模式
def check_position_side(exchange):
    """
    检查是否是单向持仓模式

    :param exchange:        交易所对象，用于获取数据
    """
    # 查询持仓模式
    res = retry_wrapper(exchange.fapiPrivate_get_positionside_dual, params={'timestamp': ''}, func_name='fapiPrivate_get_positionside_dual', if_exit=False)
    # 判断是否是单向持仓模式
    if bool(res['dualSidePosition']):  # 若当前持仓模式不是单向持仓模式，则调用接口修改持仓模式为单向持仓模式
        params = {'dualSidePosition': 'false', 'timestamp': ''}
        retry_wrapper(exchange.fapiPrivate_post_positionside_dual, params=params, func_name='fapiPrivate_post_positionside_dual', if_exit=False)
        print('修改持仓模式为单向持仓')


def fetch_binance_funding_rate(exchange, symbol, run_time, limit=1000):
    """
    获取币安的历史资金费数据
    :param exchange: 交易所对象
    :param symbol: 币种名称
    :param run_time: 运行时间
    :param limit: 请求获取多少条数据，最大1000
    """
    # start_time_dt = run_time - pd.to_timedelta('8h') * limit  # 保存最近1000条资金费数据
    param = {
        'symbol': symbol,
        # 'startTime': int(time.mktime(start_time_dt.timetuple())) * 1000,
        'limit': limit
    }
    # 获取历史数据
    funding_df = retry_wrapper(exchange.fapiPublic_get_fundingrate, params=param, func_name='获取合约历史资金费数据')
    funding_df = pd.DataFrame(funding_df)
    funding_df['fundingTime'] = pd.to_datetime(funding_df['fundingTime'].astype(float) // 1000 * 1000, unit='ms')  # 时间戳内容含有一些纳秒数据需要处理
    funding_df.sort_values('fundingTime', inplace=True)

    return funding_df
